# -*- coding: utf-8 -*-
# Calibre-Web Automated – fork of Calibre-Web
# Copyright (C) 2018-2025 Calibre-Web contributors
# Copyright (C) 2024-2025 Calibre-Web Automated contributors
# SPDX-License-Identifier: GPL-3.0-or-later
# See CONTRIBUTORS for full list of authors.

import os
import smtplib
import ssl
import threading
import socket
import mimetypes

from io import StringIO
from email.message import EmailMessage
from email.utils import formatdate, parseaddr, make_msgid
from email.generator import Generator
from flask_babel import lazy_gettext as N_

from cps.services.worker import CalibreTask
from cps.services import gmail
from cps.embed_helper import do_calibre_export
from cps import logger, config
from cps import gdriveutils
from cps.string_helper import strip_whitespaces
import uuid

log = logger.create()

CHUNKSIZE = 8192


# Class for sending email with ability to get current progress
class EmailBase:

    transferSize = 0
    progress = 0

    def data(self, msg):
        self.transferSize = len(msg)
        (code, resp) = smtplib.SMTP.data(self, msg)
        self.progress = 0
        return (code, resp)

    def send(self, strg):
        """Send `strg' to the server."""
        log.debug_no_auth('send: {}'.format(strg[:300]))
        if hasattr(self, 'sock') and self.sock:
            try:
                if self.transferSize:
                    lock = threading.Lock()
                    lock.acquire()
                    self.transferSize = len(strg)
                    lock.release()
                    for i in range(0, self.transferSize, CHUNKSIZE):
                        if isinstance(strg, bytes):
                            self.sock.send((strg[i:i + CHUNKSIZE]))
                        else:
                            self.sock.send((strg[i:i + CHUNKSIZE]).encode('utf-8'))
                        lock.acquire()
                        self.progress = i
                        lock.release()
                else:
                    self.sock.sendall(strg.encode('utf-8'))
            except socket.error:
                self.close()
                raise smtplib.SMTPServerDisconnected('Server not connected')
        else:
            raise smtplib.SMTPServerDisconnected('please run connect() first')

    @classmethod
    def _print_debug(cls, *args):
        log.debug(args)

    def getTransferStatus(self):
        if self.transferSize:
            lock2 = threading.Lock()
            lock2.acquire()
            value = int((float(self.progress) / float(self.transferSize))*100)
            lock2.release()
            return value / 100
        else:
            return 1


# Class for sending email with ability to get current progress, derived from emailbase class
class Email(EmailBase, smtplib.SMTP):

    def __init__(self, *args, **kwargs):
        smtplib.SMTP.__init__(self, *args, **kwargs)


# Class for sending ssl encrypted email with ability to get current progress, , derived from emailbase class
class EmailSSL(EmailBase, smtplib.SMTP_SSL):

    def __init__(self, *args, **kwargs):
        smtplib.SMTP_SSL.__init__(self, *args, **kwargs)


class TaskEmail(CalibreTask):
    def __init__(self, subject, filepath, attachment, settings, recipient, task_message, text, id=0, internal=False):
        super(TaskEmail, self).__init__(task_message)
        self.subject = subject
        self.attachment = attachment
        self.settings = settings
        self.filepath = filepath
        self.recipient = recipient
        self.text = text
        self.asyncSMTP = None
        self.book_id = id
        self.results = dict()

    # from calibre code:
    # https://github.com/kovidgoyal/calibre/blob/731ccd92a99868de3e2738f65949f19768d9104c/src/calibre/utils/smtp.py#L60
    def get_msgid_domain(self):
        try:
            # Parse out the address from the From line, and then the domain from that
            from_email = parseaddr(self.settings["mail_from"])[1]
            msgid_domain = strip_whitespaces(from_email.partition('@')[2])
            # This can sometimes sneak through parseaddr if the input is malformed
            msgid_domain = strip_whitespaces(msgid_domain.rstrip('>'))
        except Exception:
            msgid_domain = ''
        return msgid_domain or 'calibre-web.com'

    def prepare_message(self):
        message = EmailMessage()
        # message = MIMEMultipart()
        message['From'] = self.settings["mail_from"]
        message['To'] = self.recipient
        message['Subject'] = self.subject
        message['Date'] = formatdate(localtime=True)
        message['Message-ID'] = make_msgid(domain=self.get_msgid_domain())
        message.set_content(self.text.encode('UTF-8'), "text", "plain")
        if self.attachment:
            data = self._get_attachment(self.filepath, self.attachment)
            if data:
                # Set mimetype
                content_type, encoding = mimetypes.guess_type(self.attachment)
                if content_type is None or encoding is not None:
                    content_type = 'application/octet-stream'
                main_type, sub_type = content_type.split('/', 1)
                message.add_attachment(data, maintype=main_type, subtype=sub_type, filename=self.attachment)
            else:
                self._handleError("Attachment not found")
                return
        return message

    def run(self, worker_thread):
        try:
            # create MIME message
            msg = self.prepare_message()
            if not msg:
                return
            if self.settings['mail_server_type'] == 0:
                self.send_standard_email(msg)
            else:
                self.send_gmail_email(msg)
        except MemoryError as e:
            log.error_or_exception(e, stacklevel=3)
            self._handleError('MemoryError sending e-mail: {}'.format(str(e)))
        except (smtplib.SMTPException, smtplib.SMTPAuthenticationError) as e:
            log.error_or_exception(e, stacklevel=3)
            if hasattr(e, "smtp_error"):
                text = e.smtp_error.decode('utf-8').replace("\n", '. ')
            elif hasattr(e, "message"):
                text = e.message
            elif hasattr(e, "args"):
                text = '\n'.join(e.args)
            else:
                text = ''
            self._handleError('Smtplib Error sending e-mail: {}'.format(text))
        except (socket.error) as e:
            log.error_or_exception(e, stacklevel=3)
            self._handleError('Socket Error sending e-mail: {}'.format(e.strerror))
        except Exception as ex:
            log.error_or_exception(ex, stacklevel=3)
            self._handleError('Error sending e-mail: {}'.format(ex))

    def send_standard_email(self, msg):
        use_ssl = int(self.settings.get('mail_use_ssl', 0))
        timeout = 600  # set timeout to 5mins
        use_unverified_context = os.getenv("SMTP_ALLOW_UNVERIFIED_SSL", "false").strip().lower() in ("1", "true", "yes", "on")

        # on python3 debugoutput is caught with overwritten _print_debug function
        log.debug("Start sending e-mail")
        if use_ssl == 2:
            context = ssl.create_default_context()
            if use_unverified_context:
                context.check_hostname = False
                context.verify_mode = ssl.CERT_NONE
            self.asyncSMTP = EmailSSL(self.settings["mail_server"], self.settings["mail_port"],
                                       timeout=timeout, context=context)
        else:
            self.asyncSMTP = Email(self.settings["mail_server"], self.settings["mail_port"], timeout=timeout)

        # link to logginglevel
        if logger.is_debug_enabled():
            self.asyncSMTP.set_debuglevel(1)
        if use_ssl == 1:
            context = ssl.create_default_context()
            if use_unverified_context:
                context.check_hostname = False
                context.verify_mode = ssl.CERT_NONE
            self.asyncSMTP.starttls(context=context)
        if self.settings["mail_password_e"]:
            self.asyncSMTP.login(str(self.settings["mail_login"]), str(self.settings["mail_password_e"]))

        # Convert message to something to send
        fp = StringIO()
        gen = Generator(fp, mangle_from_=False)
        gen.flatten(msg)

        self.asyncSMTP.sendmail(self.settings["mail_from"], self.recipient, fp.getvalue())
        self.asyncSMTP.quit()
        self._handleSuccess()
        log.debug("E-mail send successfully")

    def send_gmail_email(self, message):
        gmail.send_messsage(self.settings.get('mail_gmail_token', None), message)
        self._handleSuccess()

    @property
    def progress(self):
        if self.asyncSMTP is not None:
            return self.asyncSMTP.getTransferStatus()
        else:
            return self._progress

    @progress.setter
    def progress(self, x):
        """This gets explicitly set when handle(Success|Error) are called. In this case, remove the SMTP connection"""
        if x == 1:
            self.asyncSMTP = None
            self._progress = x

    def _get_attachment(self, book_path, filename):
        """Get file as MIMEBase message"""
        calibre_path = config.get_book_path()
        extension = os.path.splitext(filename)[1][1:]
        if config.config_use_google_drive:
            df = gdriveutils.getFileFromEbooksFolder(book_path, filename)
            if df:
                datafile = os.path.join(calibre_path, book_path, filename)
                if not os.path.exists(os.path.join(calibre_path, book_path)):
                    os.makedirs(os.path.join(calibre_path, book_path))
                df.GetContentFile(datafile)
            else:
                return None
            if config.config_binariesdir and config.config_embed_metadata:
                data_path, data_file = do_calibre_export(self.book_id, extension)
                datafile = os.path.join(data_path, data_file + "." + extension)
            with open(datafile, 'rb') as file_:
                data = file_.read()
            os.remove(datafile)
        else:
            datafile = os.path.join(calibre_path, book_path, filename)
            try:
                if config.config_binariesdir and config.config_embed_metadata:
                    data_path, data_file = do_calibre_export(self.book_id, extension)
                    datafile = os.path.join(data_path, data_file + "." + extension)
                with open(datafile, 'rb') as file_:
                    data = file_.read()
                if config.config_binariesdir and config.config_embed_metadata:
                    os.remove(datafile)
            except IOError as e:
                log.error_or_exception(e, stacklevel=3)
                log.error('The requested file could not be read. Maybe wrong permissions?')
                return None
        return data

    @property
    def name(self):
        return N_("E-mail")

    @property
    def is_cancellable(self):
        return False

    def __str__(self):
        return "E-mail {}, {}".format(self.name, self.subject)
